Watershed-based tree segmentation¶
Watershed segmentation, also known as watershed transformation, is an image processing technique used to segment regions of interest in digital images. This technique is widely used in computer vision and image processing for object detection and segmentation, especially in images where the boundaries of objects are not well defined.
The main idea of watershed segmentation is to treat the image as a topographic map, where peaks represent regions that need to be segmented. The process involves analyzing intensity features of the image to identify regions of interest and separating them into different regions or segments.
These are the main steps of the watershed segmentation algorithm:
Gradient transformation: First, the gradient of the image is calculated. This is done to highlight the edges or boundaries between objects in the image. There are several ways to calculate the gradient, such as the Sobel or Canny operator.
Marker identification: Next, initial markers are chosen, which correspond to well-defined points or areas of interest in the image. These markers can be chosen manually or automatically depending on the application.
Watershed transformation: The watershed transformation is applied to the identified markers and the gradient of the image. In this process, the markers act as seeds that generate a flood in the image. The flood starts at the markers and spreads throughout the image, creating separate regions and associating pixels with those regions.
Final segmentation: After the watershed transformation we obtain an initial segmentation of the image. However, this segmentation may contain overlapping regions. To correct this, a region merging step is performed based on specific criteria such as distance or intensity difference.
Watersheed segmentation is particularly useful in images with overlapping objects or in regions where the edges are poorly defined. However, one of the challenges of this technique is its sensitivity to noise, which can lead to inaccurate segmentations. To overcome this problem, pre- and post-processing can be applied to improve the quality of the results.
In addition, the watershed transform can also be combined with other segmentation techniques, such as region-based segmentation or region-growing segmentation, to obtain better results in different types of images and applications.
Let's apply watersheed segmentation to a Drone image to obtain the polygons of each plant.
!pip install rasterio
Collecting rasterio Downloading rasterio-1.3.10-cp310-cp310-manylinux2014_x86_64.whl.metadata (14 kB) Collecting affine (from rasterio) Downloading affine-2.4.0-py3-none-any.whl.metadata (4.0 kB) Requirement already satisfied: attrs in /usr/local/lib/python3.10/dist-packages (from rasterio) (24.2.0) Requirement already satisfied: certifi in /usr/local/lib/python3.10/dist-packages (from rasterio) (2024.7.4) Requirement already satisfied: click>=4.0 in /usr/local/lib/python3.10/dist-packages (from rasterio) (8.1.7) Requirement already satisfied: cligj>=0.5 in /usr/local/lib/python3.10/dist-packages (from rasterio) (0.7.2) Requirement already satisfied: numpy in /usr/local/lib/python3.10/dist-packages (from rasterio) (1.26.4) Collecting snuggs>=1.4.1 (from rasterio) Downloading snuggs-1.4.7-py3-none-any.whl.metadata (3.4 kB) Requirement already satisfied: click-plugins in /usr/local/lib/python3.10/dist-packages (from rasterio) (1.1.1) Requirement already satisfied: setuptools in /usr/local/lib/python3.10/dist-packages (from rasterio) (71.0.4) Requirement already satisfied: pyparsing>=2.1.6 in /usr/local/lib/python3.10/dist-packages (from snuggs>=1.4.1->rasterio) (3.1.2) Downloading rasterio-1.3.10-cp310-cp310-manylinux2014_x86_64.whl (21.5 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 21.5/21.5 MB 37.3 MB/s eta 0:00:00 Downloading snuggs-1.4.7-py3-none-any.whl (5.4 kB) Downloading affine-2.4.0-py3-none-any.whl (15 kB) Installing collected packages: snuggs, affine, rasterio Successfully installed affine-2.4.0 rasterio-1.3.10 snuggs-1.4.7
from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive
from skimage import io
from skimage.feature import peak_local_max
from scipy import ndimage as ndi
from skimage.segmentation import watershed
import matplotlib.pyplot as plt
import numpy as np
import cv2
import rasterio
from skimage.segmentation import mark_boundaries
from rasterio.plot import show
Let's set the image path, open and display it with matplotlib:
path_img = '/content/drive/MyDrive/Datasets_CV/AOI_teste.tif'
src = rasterio.open(path_img)
img = src.read()
img = img.transpose([1,2,0])
img = img.astype('uint8')
plt.figure(figsize=[16,16])
plt.imshow(img)
<matplotlib.image.AxesImage at 0x788495fb1510>
We can convert the image to BGR with opencv and apply a median blur filter to this image:
bgr_img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
resulting_image = cv2.medianBlur(bgr_img ,35)
plt.figure(figsize=[12,12])
plt.imshow(resulting_image)
plt.axis('off')
(-0.5, 4555.5, 6263.5, -0.5)
We will then convert this image to HSV color space and filter out the green color. We will create a mask with the green color.
hsv_img = cv2.cvtColor(resulting_image, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv_img, (36, 50, 50), (70, 255,255))
imask = mask>0
green = np.zeros_like(img, np.uint8)
green[imask] = img[imask]
plt.figure(figsize=[12,12])
plt.imshow(green)
plt.axis('off')
(-0.5, 4555.5, 6263.5, -0.5)
With the mask we will apply morphological filters to generate regions in the image:
mask = np.where(mask == 255, 1,0)
mask = mask.astype('uint8')
plt.figure(figsize=[12,12])
plt.imshow(mask)
plt.axis('off')
(-0.5, 4555.5, 6263.5, -0.5)
dilate_se=cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
result = cv2.dilate(mask,dilate_se,iterations = 5)
plt.figure(figsize=[12,12])
plt.imshow(result)
plt.axis('off')
(-0.5, 4555.5, 6263.5, -0.5)
erosion_se=cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
erosion = cv2.erode(result,erosion_se,iterations = 28)
plt.figure(figsize=[12,12])
plt.imshow(erosion)
plt.axis('off')
(-0.5, 4555.5, 6263.5, -0.5)
We will apply the distance transform to obtain the image of the regions with the distance from the edge:
dist_transform = cv2.distanceTransform(erosion, cv2.DIST_L2,3)
plt.figure(figsize=[12,12])
plt.imshow(dist_transform)
plt.axis('off')
(-0.5, 4555.5, 6263.5, -0.5)
Then we filter the core areas:
ret, sure_fg = cv2.threshold(dist_transform,0.3*dist_transform.max(),255,0)
plt.figure(figsize=[12,12])
plt.imshow(sure_fg)
plt.axis('off')
(-0.5, 4555.5, 6263.5, -0.5)
We obtain the unique segments of these core areas and apply the Watershed algorithm:
markers, _ = ndi.label(sure_fg)
segmented = watershed(255-dist_transform, markers, mask=mask)
With the generated segments we convert them into polygons using the georeferencing of the original image as a base:
segments_quick = segmented.astype('uint16')
segments_quick
array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], dtype=uint16)
mask_seg = np.where(segments_quick == 0, 0,1)
mask_seg = mask_seg.astype('uint8')
from rasterio import features
from shapely.geometry import shape
import geopandas as gpd
import shapely
find_shapes = features.shapes(segments_quick, mask=mask_seg, transform=src.transform)
polygons = [shapely.geometry.Polygon(shape[0]["coordinates"][0]) for shape in find_shapes]
crs = src.crs
Poly_gdf = gpd.GeoDataFrame(crs=crs, geometry=polygons)
Poly_gdf
| geometry | |
|---|---|
| 0 | POLYGON ((394372.805 7306534.973, 394372.805 7... |
| 1 | POLYGON ((394377.860 7306528.161, 394377.860 7... |
| 2 | POLYGON ((394374.110 7306531.056, 394374.110 7... |
| 3 | POLYGON ((394369.410 7306527.828, 394369.410 7... |
| 4 | POLYGON ((394377.908 7306528.256, 394377.908 7... |
| ... | ... |
| 238 | POLYGON ((394439.217 7306410.384, 394439.217 7... |
| 239 | POLYGON ((394430.791 7306408.889, 394430.791 7... |
| 240 | POLYGON ((394442.754 7306406.895, 394442.754 7... |
| 241 | POLYGON ((394433.710 7306405.423, 394433.710 7... |
| 242 | POLYGON ((394436.986 7306402.931, 394436.986 7... |
243 rows × 1 columns
We present the polygons:
Poly_gdf.boundary.plot(figsize=(20,14))
<Axes: >
We can present the polygons together with the image:
Poly_gdf.reset_index(inplace=True)
fig, ax = plt.subplots(figsize=(15, 15))
with rasterio.open(path_img) as src:
show(src,ax=ax)
Poly_gdf.plot('index', ax=ax, cmap='hsv', alpha=0.5)
Poly_gdf.boundary.plot(ax=ax, edgecolor='red')
<Axes: >